Mutations
Unlike queries, mutations are typically used to create/update/delete data or perform server side-effects. For this purpose, TanStack Query exports a useMutation hook.
Here's an example of a mutation that adds a new todo to the server:
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:http/http.dart' as http;
class CreateTodo extends HookWidget {
const CreateTodo({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final mutation = useMutation<Map<String, dynamic>, Map<String, dynamic>>(
mutationFn: (newTodo) async {
final res = await http.post(
Uri.parse('/todos'),
body: jsonEncode(newTodo),
headers: {'Content-Type': 'application/json'},
);
return jsonDecode(res.body) as Map<String, dynamic>;
},
);
if (mutation.isPending) {
return const Text('Adding todo...');
}
return Column(
children: [
if (mutation.isError) Text('An error occurred: ${mutation.error}'),
if (mutation.isSuccess) const Text('Todo added!'),
ElevatedButton(
onPressed: () => mutation.mutate({'id': DateTime.now().toIso8601String(), 'title': 'Do Laundry'}),
child: const Text('Create Todo'),
),
],
);
}
}
A mutation can only be in one of the following states at any given moment:
isIdleorstatus == MutationStatus.idle- The mutation is currently idle or in a fresh/reset stateisPendingorstatus == MutationStatus.pending- The mutation is currently runningisErrororstatus == MutationStatus.error- The mutation encountered an errorisSuccessorstatus == MutationStatus.success- The mutation was successful and mutation data is available
Beyond those primary states, more information is available depending on the state of the mutation:
error- If the mutation is in anerrorstate, the error is available via theerrorproperty.data- If the mutation is in asuccessstate, the data is available via thedataproperty.
In the example above, you also saw that you can pass variables to your mutations function by calling the mutate function with a single variable or object.
IMPORTANT: The
mutatefunction is an asynchronous function, which means you cannot use it directly in an event callback in React 16 and earlier. If you need to access the event inonSubmityou need to wrapmutatein another function. This is due to React event pooling.
// In Flutter there's no React event pooling; you can call mutate directly from handlers.
class CreateTodoForm extends HookWidget {
const CreateTodoForm({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final mutation = useMutation<Map<String, dynamic>, Map<String, dynamic>>(
mutationFn: (formData) async {
final res = await http.post(
Uri.parse('/api'),
body: jsonEncode(formData),
headers: {'Content-Type': 'application/json'},
);
return jsonDecode(res.body) as Map<String, dynamic>;
},
);
void onSubmit(Map<String, dynamic> formData) {
mutation.mutate(formData);
}
return Form(
child: Column(
children: [
TextField(onSubmitted: (value) => onSubmit({'title': value})),
ElevatedButton(onPressed: () => onSubmit({'title': 'Example'}), child: const Text('Submit')),
],
),
);
}
}
Resetting Mutation State
It's sometimes the case that you need to clear the error or data of a mutation request. To do this, you can use the reset function to handle this:
class CreateTodoForm extends HookWidget {
const CreateTodoForm({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final title = useState('');
final mutation = useMutation<Map<String, dynamic>, Map<String, dynamic>>(
mutationFn: (vars) async {
final res = await http.post(Uri.parse('/api/todos'), body: jsonEncode(vars), headers: {'Content-Type': 'application/json'});
return jsonDecode(res.body) as Map<String, dynamic>;
},
);
void onCreateTodo() {
mutation.mutate({'title': title.value});
}
return Form(
child: Column(
children: [
if (mutation.error != null)
GestureDetector(onTap: () => mutation.reset(), child: Text('${mutation.error}')),
TextField(onChanged: (v) => title.value = v),
ElevatedButton(onPressed: onCreateTodo, child: const Text('Create Todo')),
],
),
);
}
}
Mutation Side Effects
useMutation comes with some helper options that allow quick and easy side-effects at any stage during the mutation lifecycle. These come in handy for both invalidating and refetching queries after mutations and even optimistic updates
Optimistic Updates
useMutation<Map<String, dynamic>, Map<String, dynamic>>(
mutationFn: addTodo,
onMutate: () {
// A mutation is about to happen!
// Optionally prepare optimistic update
},
onError: (error) {
// An error happened!
print('rolling back optimistic update');
},
onSuccess: (data) {
// Boom baby!
},
onSettled: (data, error) {
// Error or success... doesn't matter!
},
);
When returning a promise in any of the callback functions it will first be awaited before the next callback is called:
useMutation<Map<String, dynamic>, Map<String, dynamic>>(
mutationFn: addTodo,
onSuccess: (data) {
print("I'm first!");
},
onSettled: (data, error) {
print("I'm second!");
},
);
You might find that you want to trigger additional callbacks beyond the ones defined on useMutation when calling mutate. This can be used to trigger component-specific side effects. To do that, you can provide any of the same callback options to the mutate function after your mutation variable. Supported options include: onSuccess, onError and onSettled. Please keep in mind that those additional callbacks won't run if your component unmounts before the mutation finishes.
final mutation = useMutation<Map<String, dynamic>, Map<String, dynamic>>(
mutationFn: addTodo,
onSuccess: (data) {
// I will fire first
},
onError: (error) {
// I will fire first
},
onSettled: (data, error) {
// I will fire first
},
);
// Provide per-call callbacks via MutateOptions to mutateAsync
await mutation.mutateAsync(todo, MutateOptions(
onSuccess: (data) {
// I will fire second!
},
onError: (error) {
// I will fire second!
},
onSettled: (data, error) {
// I will fire second!
},
));
Consecutive mutations
There is a slight difference in handling onSuccess, onError and onSettled callbacks when it comes to consecutive mutations. When passed to the mutate function, they will be fired up only once and only if the component is still mounted. This is due to the fact that mutation observer is removed and resubscribed every time when the mutate function is called. On the contrary, useMutation handlers execute for each mutate call.
Be aware that most likely,
mutationFnpassed touseMutationis asynchronous. In that case, the order in which mutations are fulfilled may differ from the order ofmutatefunction calls.
useMutation<Map<String, dynamic>, String>(
mutationFn: addTodo,
onSuccess: (data) {
// Will be called 3 times
},
);
final todos = ['Todo 1', 'Todo 2', 'Todo 3'];
for (final todo in todos) {
// Per-call callbacks can be passed to mutateAsync
mutation.mutateAsync(todo, MutateOptions(
onSuccess: (data) {
// Will execute only once, for the last mutation (Todo 3),
// regardless which mutation resolves first
},
));
}
Promises
Use mutateAsync instead of mutate to get a promise which will resolve on success or throw on an error. This can for example be used to compose side effects.
final mutation = useMutation<Map<String, dynamic>, Map<String, dynamic>>(mutationFn: addTodo);
try {
final todo = await mutation.mutateAsync({'title': 'Do Laundry'});
print(todo);
} catch (error) {
print(error);
} finally {
print('done');
}
Retry
By default, TanStack Query will not retry a mutation on error, but it is possible with the retry option:
final mutation = useMutation<Map<String, dynamic>, Map<String, dynamic>>(
mutationFn: addTodo,
retry: 3,
);
If mutations fail because the device is offline, they will be retried in the same order when the device reconnects.
Persist mutations
Mutations can be persisted to storage if needed and resumed at a later point. This can be done with the hydration functions:
final queryClient = QueryClient(
mutationCache: MutationCache(
config: MutationCacheConfig(
onMutate: () {
// Cancel current queries for the todos list
// Create optimistic todo
// Add optimistic todo to todos list via queryClient.setQueryInfiniteData / setQueryData
},
onSuccess: (result) {
// Replace optimistic todo in the todos list with the result
},
onError: (error) {
// Remove optimistic todo from the todos list
},
),
),
);
// Start mutation in some component:
final mutation = useMutation<Map<String, dynamic>, Map<String, dynamic>>(mutationFn: addTodo);
mutation.mutate({'title': 'title'});
// Note: Dehydrate / hydrate APIs and resumePausedMutations may differ in Dart; use equivalent helpers if available.